import expect from "expect"
import SwaggerUi from "swagger-ui"
import ValidateBasePlugin from "plugins/validate-base"
import ValidateSemanticPlugin from "plugins/validate-semantic"
import ASTPlugin from "plugins/ast"

function getSystem(spec) {
  return new Promise((resolve) => {
    const system = SwaggerUi({
      spec,
      domNode: null,
      presets: [
        SwaggerUi.plugins.SpecIndex,
        SwaggerUi.plugins.ErrIndex,
        SwaggerUi.plugins.DownloadUrl,
        SwaggerUi.plugins.SwaggerJsIndex,
      ],
      initialState: {
        layout: undefined
      },
      plugins: [
        ASTPlugin,
        ValidateBasePlugin,
        ValidateSemanticPlugin,
        () => ({
          statePlugins: {
            configs: {
              actions: {
                loaded: () => {
                  return {
                    type: "noop"
                  }
                }
              }
            }
          }
        })
      ]
    })
    resolve(system)
  })
}

describe("validation plugin - selectors", function() {
  this.timeout(10 * 1000)

  it("allSchemas should pick up parameter schemas", () => {
    const spec = {
      paths: {
        test: {
          parameters: [{
            name: "common"
          }],
          get: {
            parameters: [{
              name: "tags"
            }]
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(2)
        expect(nodes[0].path).toEqual(["paths","test","parameters","0"])
        expect(nodes[1].path).toEqual(["paths","test","get","parameters","0"])
      })
  })

  it("allSchemas should pick up response schemas", () => {
    const spec = {
      paths: {
        test: {
          get: {
            responses: {
              "200": {
                schema: {
                  type: "string"
                }
              }
            }
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(1)
        expect(nodes[0].path.join(".")).toEqual("paths.test.get.responses.200.schema")
      })
  })

  it("allSchemas should pick up definitions", () => {
    const spec = {
      definitions: {
        fooModel: {
          type: "object",
          properties: {
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(1)
        expect(nodes[0].key).toEqual("fooModel")
      })
  })

  it("allSchemas should pick up headers", () => {
    const spec = {
      paths: {
        test: {
          get: {
            responses: {
              "200": {
                headers: {
                  foo: {
                    "type": "integer"
                  }
                }
              }
            }
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(1)
        expect(nodes[0].path.join(".")).toEqual("paths.test.get.responses.200.headers.foo")
      })
  })

  it("allSchemas should pick up subschemas in properties", () => {
    const spec = {
      definitions: {
        fooModel: {
          type: "object",
          properties: {
            foo: {
              type: "string"
            }
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(2)
        expect(nodes[0].key).toEqual("fooModel")
        expect(nodes[1].key).toEqual("foo")
      })
  })

  it("allSchemas should pick up subschemas in additionalProperties - simple", () => {
    const spec = {
      definitions: {
        fooModel: {
          type: "object",
          additionalProperties: {
            type: "string"
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(2)
        expect(nodes[0].key).toEqual("fooModel")
        expect(nodes[1].key).toEqual("additionalProperties")
      })
  })

  it("allSchemas should pick up subschemas in additionalProperties - complex", () => {
    const spec = {
      definitions: {
        fooModel: {
          type: "object",
          additionalProperties: {
            type: "object",
            properties: {
              foo: {
                type: "string"
              }
            }
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(3)
        expect(nodes[0].key).toEqual("fooModel")
        expect(nodes[1].key).toEqual("additionalProperties")
        expect(nodes[2].key).toEqual("foo")
      })
  })

  it("allSchemas should pick up subschemas in array", () => {
    const spec = {
      definitions: {
        fooModel: {
          type: "array",
          items: {
            type: "string"
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(2)
        expect(nodes[0].key).toEqual("fooModel")
        expect(nodes[1].key).toEqual("items")
      })
  })

  it("allSchemas should pick up subschemas in array of objects", () => {
    const spec = {
      definitions: {
        fooModel: {
          type: "array",
          items: {
            type: "object",
            properties: {
              foo: {
                type: "string"
              }
            }
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(3)
        expect(nodes[0].key).toEqual("fooModel")
        expect(nodes[1].key).toEqual("items")
        expect(nodes[2].key).toEqual("foo")
      })
  })
  it("allSchemas should pick up OAS3 request and response schemas", () => {
    const spec = {
      "openapi": "3.0.0",
      "paths": {
        "/ping": {
          "post": {
            "requestBody": {
              "content": {
                "application/myRequestMediaType": {
                  "schema": {
                    "type": "array"
                  }
                }
              }
            },
            "responses": {
              "200": {
                "description": "OK",
                "content": {
                  "application/myResponseMediaType": {
                    "schema": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }

    return getSystem(spec)
      .then(system => system.validateSelectors.allSchemas())
      .then(nodes => {
        expect(nodes.length).toEqual(2)
        expect(nodes[0].node).toNotBe(nodes[1].node)
        expect(nodes[0].key).toEqual("schema")
        expect(nodes[0].parent.key).toEqual("application/myRequestMediaType")
        expect(nodes[1].key).toEqual("schema")
        expect(nodes[1].parent.key).toEqual("application/myResponseMediaType")
      })
  })

  describe("allResponses", function() {
    it("should pick up operation responses with specific codes like 200", () => {
      const spec = {
        paths: {
          "/foo": {
            get: {
              responses: {
                "200": {
                  description: "ok"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["paths","/foo","get","responses","200"])
        })
    })
    it("should pick up the 'default' response", () => {
      const spec = {
        paths: {
          "/foo": {
            get: {
              responses: {
                default: {
                  description: "ok"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["paths","/foo","get","responses","default"])
        })
    })
    it("should pick up OAS3 response code ranges like 2XX", () => {
      const spec = {
        openapi: "3.0.0",
        paths: {
          "/foo": {
            get: {
              responses: {
                "2XX": {
                  description: "ok"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["paths","/foo","get","responses","2XX"])
        })
    })
    it("should ignore x- extension keys in an operation's `responses` section", () => {
      const spec = {
        paths: {
          "/foo": {
            get: {
              responses: {
                "x-ext": {
                  foo: "bar"
                }
              }
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
    it("should pick up OpenAPI 2 global response definitions", () => {
      const spec = {
        swagger: "2.0",
        responses: {
          OkResponse: {
            description: "ok"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["responses","OkResponse"])
        })
    })
    it("should pick up OpenAPI 2 response definitions named 'x-'", () => {
      const spec = {
        swagger: "2.0",
        responses: {
          "x-MyResponse": {
            description: "ok"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["responses","x-MyResponse"])
        })
    })
    it("should pick up OpenAPI 3 response components", () => {
      const spec = {
        openapi: "3.0.0",
        components : {
          responses: {
            OkResponse: {
              description: "ok"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["components","responses","OkResponse"])
        })
    })
    it("should pick up OpenAPI 3 response components named 'x-'", () => {
      const spec = {
        openapi: "3.0.0",
        components : {
          responses: {
            "x-MyResponse": {
              description: "ok"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allResponses())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].path).toEqual(["components","responses","x-MyResponse"])
        })
    })
  })

  describe("allSecurityDefinitions", () => {
    it("should pick up OAS2 security schemes", () => {
      const spec = {
        swagger: "2.0",
        securityDefinitions: {
          basicAuth: {
            type: "basic"
          },
          apiKeyAuth: {
            type: "apiKey"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].key).toEqual("basicAuth")
          expect(nodes[1].key).toEqual("apiKeyAuth")
        })
    })
    it("should pick up OAS3 security schemes", () => {
      const spec = {
        openapi: "3.0.0",
        components: {
          securitySchemes: {
            basicAuth: {
              type: "http"
            },
            apiKeyAuth: {
              type: "apiKey"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].key).toEqual("basicAuth")
          expect(nodes[1].key).toEqual("apiKeyAuth")
        })
    })
    it("should pick up OAS2 security schemes named x- (i.e. not consider them extensions)", () => {
      const spec = {
        swagger: "2.0",
        securityDefinitions: {
          "x-auth": {
            type: "basic"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].key).toEqual("x-auth")
        })
    })
    it("should pick up OAS3 security schemes named x- (i.e. not consider them extensions)", () => {
      const spec = {
        openapi: "3.0.0",
        components: {
          securitySchemes: {
            "x-auth": {
              type: "basic"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(1)
          expect(nodes[0].key).toEqual("x-auth")
        })
    })
    it("should not pick up arbitrary OAS2 nodes named `securityDefinitions`", () => {
      const spec = {
        swagger: "2.0",
        definitions: {
          securityDefinitions: {
            type: "object"
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
    it("should not pick up arbitrary OAS3 nodes named `securitySchemes`", () => {
      const spec = {
        openapi: "3.0.0",
        components: {
          schemas: {
            securitySchemes: {
              type: "object"
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityDefinitions())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
  })

  describe("allSecurityRequirements", () => {
    it("should pick up global security requirements", () => {
      const spec = {
        security: [
          {
            auth1: []
          },
          {
            auth2: [],
            auth3: []
          }
        ]
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].node).toEqual(
            { "auth1": [] }
          )
          expect(nodes[1].node).toEqual(
            { "auth2": [], "auth3": [] }
          )
        })
    })
    it("should pick up operation-level security requirements", () => {
      const spec = {
        paths: {
          "/": {
            get: {
              security: [
                {
                  auth1: []
                },
                {
                  auth2: [],
                  auth3: []
                }
              ]
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].node).toEqual(
            { "auth1": [] }
          )
          expect(nodes[1].node).toEqual(
            { "auth2": [], "auth3": [] }
          )
        })
    })
    it("should pick up empty security requirements", () => {
      const spec = {
        security: [
          {}
        ],
        paths: {
          "/": {
            get: {
              security: [
                {}
              ]
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(2)
          expect(nodes[0].node).toNotBe(nodes[1].node)
          expect(nodes[0].node).toEqual({})
          expect(nodes[1].node).toEqual({})
        })
    })
    it("should not pick up the `security` key inside extensions", () => {
      const spec = {
        paths: {
          "/": {
            "x-ext1": {
              security: [
                { foo: [] }
              ]
            }
          },
          "x-ext2": {
            get: {
              security: [
                { bar: [] }
              ]
            }
          }
        }
      }

      return getSystem(spec)
        .then(system => system.validateSelectors.allSecurityRequirements())
        .then(nodes => {
          expect(nodes.length).toEqual(0)
        })
    })
  })
})
